package base;

import agents.Agent;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EnumMap;
import java.util.List;
import java.util.LinkedList;

import edu.uci.ics.jung.graph.*;
import edu.uci.ics.jung.io.graphml.*;
import org.apache.commons.collections15.Transformer;


import plots.Plot;
import plots.Edge;
import plots.PlotRegenerator;
import plots.PlotFactory;

import static base.Environment.Configurations.*;

/**
 * The agents' resource-rich environment.
 */
public class Environment {
	
	public enum Configurations {
		MAX_YIELD,		
		PLOTS_FILE,
		PLOT_REGENERATOR,
		MAX_SIMULATION_STEPS,
		STATIC_AGENTS,
		ADAPTIVE_AGENTS,
		QUOTA,
		ADAPTIVE_QUOTA_FACTOR,
		VARY_QUOTA_FACTOR,
		UTILITY_HISTORY_SIZE,
		QUOTA_INC_THRESHOLD,
		QUOTA_DEC_THRESHOLD,
		AGENT_MEMORY,
		STRATEGY_GENERATOR,
		STATIC_SELECTOR,
		ADAPTIVE_SELECTOR,
		INITIAL_EXP,
		EXP_DECREASE,
		NEW_THRESHOLD,
		DEL_THRESHOLD,
		PLOT_UPD_MULTIPLIER,
		CHUNK_FACTOR,
		MIN_USAGES_FACTOR,
		MAX_STRAT_NO,
		TARGET_UPD_FACTOR,
		PATH_UPD_FACTOR,
		QUOTA_RECHECK_FACTOR,
		POO_DISPERSION_FACTOR,
		REGEN_THRESHOLD,
		QUOTA_STDDEV_THRESHOLD,
		DEBUG_MODE,
		UI_MODE,
		GRAPH_LAYOUT
	}
	
	
	private static final String CONF_DIR       = "conf";
	private static final String DEFAULT_CONFIG = CONF_DIR + File.separator + "default.config";
	private static final String DEFAULT_PLOTS  = CONF_DIR + File.separator + "default.plots";
	private static final String WHITE          = "[\n\t\f\r ]*";
	private static final String SPLIT          = WHITE + "=" + WHITE;
	private static final String COMMENT        = WHITE + "#.*";
	
	protected int step;
	protected UndirectedGraph<Plot, Edge> plots;	
	protected List<Agent> agents;
	protected EnumMap<Configurations, String> config;
	protected PlotRegenerator plotRegenerator;
	protected double[][] plotDistances;
	protected int[][] plotNext;
	
	public Environment() throws Exception {
		this.step   = -1;
		this.config = loadConfig(DEFAULT_CONFIG);
		System.out.println("Thus I have loaded 1");
		String plotsFile = config.containsKey(PLOTS_FILE) ? getString(PLOTS_FILE) : DEFAULT_PLOTS;
		this.plots  = loadPlots(plotsFile);
		this.computeDistances();
		this.agents = new LinkedList<Agent>();
		this.plotRegenerator = PlotFactory.newRegenerator(getString(PLOT_REGENERATOR));		
	}
	
	public Environment(String configFile) throws Exception {
		this.step   = -1;
		this.config = loadConfig(DEFAULT_CONFIG);
		System.out.println("Thus I have loaded 2");
		this.config.putAll(loadConfig(configFile));
		String plotsFile = config.containsKey(PLOTS_FILE) ? getString(PLOTS_FILE) : DEFAULT_PLOTS;
		this.plots  = loadPlots(plotsFile);
		this.computeDistances();
		this.agents = new LinkedList<Agent>();		
		this.plotRegenerator = PlotFactory.newRegenerator(getString(PLOT_REGENERATOR));		
	}
	
	
	private static final UndirectedGraph<Plot, Edge> loadPlots(String plotsFile) throws Exception {
		GraphMLReader2<UndirectedGraph<Plot,Edge>, Plot, Edge> gmlr;		
		
		//	TODO - finish this and write graphml for given graph
		
		Transformer<NodeMetadata, Plot> plotTrans = new Transformer<NodeMetadata,Plot>(){
            public Plot transform(NodeMetadata nmd ){
            	Plot v;
            
            	if (!nmd.getProperty("vid").equals("na"))    
            		v = new Plot(Integer.parseInt(nmd.getProperty("id")),
            				Double.parseDouble(nmd.getProperty("maxYield")));            		
            	else
            		v = new Plot(Double.parseDouble(nmd.getProperty("vy")));
            	
                return v;
            }
		};
		
		Transformer<EdgeMetadata, Edge> edgeTrans = new Transformer<EdgeMetadata,Edge>(){
            public Edge transform( EdgeMetadata emd ){
                    Edge e = new Edge(Double.parseDouble(emd.getProperty("ev")));
                    return e;
            }
		};
    
		Transformer<HyperEdgeMetadata, Edge> hyperEdgeTrans = new Transformer<HyperEdgeMetadata,Edge>(){
            public Edge transform( HyperEdgeMetadata emd ){
            	return new Edge(Integer.parseInt(emd.getProperty("ev")));
            }
		};
		
		Transformer< GraphMetadata , UndirectedGraph<Plot, Edge>> graphTrans = new Transformer<GraphMetadata,UndirectedGraph<Plot, Edge>>(){
            public UndirectedGraph<Plot,Edge> transform( GraphMetadata gmd ){
                    return new UndirectedSparseGraph<Plot,Edge>();
            }
		};		
		
		gmlr = new GraphMLReader2<UndirectedGraph<Plot,Edge>, Plot, Edge>(new FileReader(plotsFile), 
				graphTrans, plotTrans, edgeTrans, hyperEdgeTrans);
		
		System.out.println("Started to load");
		
		//BufferedReader tRead = new BufferedReader(new FileReader(plotsFile));
		//System.out.println(tRead.readLine());
		return gmlr.readGraph();
	}
	
	private static final EnumMap<Configurations, String> loadConfig(String configFile)
			throws Exception {
		
		BufferedReader br = null;
		EnumMap<Configurations, String> config =
				new EnumMap<Configurations, String>(Configurations.class);
		
		try {
			br = new BufferedReader(new FileReader(configFile));
			String line;
			int n = 0;
			
			while ((line = br.readLine()) != null)
				if (!line.isEmpty() && !line.matches(WHITE) && !line.matches(COMMENT)) {
					n++;
					String[] splits = line.split(SPLIT);
					
					if (splits.length != 2)
						throw new Exception("Malformed config file at line " + n);
					
					System.out.println(splits[0] + " - " + splits[1]);
					
					if (config.containsKey(Configurations.valueOf(splits[0])))
						throw new Exception("Duplicate parameter '" +
											Configurations.valueOf(splits[0]) +
											"' at line " + n);
					
					config.put(Configurations.valueOf(splits[0]), splits[1]);
				}
			
		} finally {
			if (br != null)
				try { br.close(); }
				catch (IOException e) {}
		}
		
		return config;
	}
	
/** 
 * Computes distances between plots using Floyd-Warshall
 * Also computes matrix that aids in retrieving the shortest path between two nodes.
 * NOTE: Assumes that plot ids are from 0 and increasing, with no gaps in-between;  
 */
	private void computeDistances(){
		plotDistances = new double[this.plots.getVertexCount()][this.plots.getVertexCount()];
		plotNext = new int[this.plots.getVertexCount()][this.plots.getVertexCount()];
		Plot[] auxPlots = plots.getVertices().toArray(new Plot[0]);
		
		for(int i=0; i < auxPlots.length; i++)
			for(int j=0; j < auxPlots.length; j++){
				if (this.plots.isNeighbor(auxPlots[i], auxPlots[j]))
					plotDistances[i][j] = this.plots.findEdge(auxPlots[i], auxPlots[j]).getValue();
				else if (i==j)
					plotDistances[i][j] = 0;
				else
					plotDistances[i][j] = Double.MAX_VALUE;
				plotNext[i][j] = -1;
			}		
		
		for(int i=0; i < auxPlots.length; i++)
			for(int j=0; j < auxPlots.length; j++)
				for(int k=0; k < auxPlots.length; k++)
					if (plotDistances[i][j] > plotDistances[i][k] + plotDistances[k][j]){
						plotDistances[i][j] = plotDistances[i][k] + plotDistances[k][j];
						plotNext[i][j] = k;
					}				
	}
	
	public int getInt(Configurations element) {
		if (!config.containsKey(element))
			throw new RuntimeException("Parameter '" + element + "' not found");
		return Integer.parseInt(config.get(element));
	}
	
	public double getDouble(Configurations element) {
		if (!config.containsKey(element))
			throw new RuntimeException("Parameter '" + element + "' not found");
		return Double.parseDouble(config.get(element));
	}
	
	public String getString(Configurations element) {
		if (!config.containsKey(element))
			throw new RuntimeException("Parameter '" + element + "' not found");
		return config.get(element);
	}
	
	public boolean getBoolean(Configurations element) {
		if (!config.containsKey(element))
			throw new RuntimeException("Parameter '" + element + "' not found");
		return Boolean.parseBoolean(config.get(element));
	}
	
	public final void regenerate() {
		step++;
		for (Plot x:plots.getVertices())
			plotRegenerator.regenerate(x);
	}
	
	public int getSize() {		
		return plots.getVertexCount();
	}
	
	public int getStep() {
		return step;
	}
	
	public UndirectedGraph<Plot, Edge> getPlots() {
		return plots;
	}
	
	/**
	 * Returns the adjacency matrix with smallest distances between plots.
	 * @return
	 * The above matrix
	 */
	public double[][] getDistances(){
		return plotDistances;
	}
	
	public double getDistanceBetween(Plot x, Plot y){
		return plotDistances[x.hashCode()][y.hashCode()];
	}
	
	public double getMaxDistanceTo(Plot x){
		double result = 0;
		
		for(Edge a:plots.getIncidentEdges(x))
			if (a.getValue() > result)
				result = a.getValue();
		
		return result;
	}
	
	/*
	 * Generates intermediate path nodes between x and y
	 */
	private ArrayList<Integer> getPath(int x, int y){
		ArrayList<Integer> result = new ArrayList<Integer>();
		
		if (plotNext[x][y] >= 0){
			result.addAll(getPath(x, plotNext[x][y]));
			result.add(plotNext[x][y]);
			result.addAll(getPath(plotNext[x][y], y));
		}
		return result;
	}
	
	/**
	 * Returns the shortest path between two plots
	 * @param x - first node
	 * @param y - second node
	 * @return list of ordered plots that are between x and y, exclusive of them;
	 */
	public ArrayList<Plot> getPath(Plot x, Plot y){
		if (x.equals(y)) {
			return new ArrayList<Plot>();
		}
		
		ArrayList<Plot> result = new ArrayList<Plot>();
		ArrayList<Plot> itPlots = new ArrayList<Plot>(plots.getVertices());
		ArrayList<Integer> path = getPath(itPlots.indexOf(x), itPlots.indexOf(y));
		
		for(Integer i : path)
			result.add(itPlots.get(i));	
		
		return result;
	}

	
	public void registerAgent(Agent agent) {
		agents.add(agent);
	}
	
	public List<Agent> getActiveAgents() {
		return agents;
	}
	
	/**
	 * Returns the average agent quota.
	 * 
	 * @return the average agent quota.
	 */
	public double getQuotaNorm() {
		if (agents.size() == 0)
			return 0D;
		
		double sum = 0D;
		for (Agent agent : agents) {
			sum += agent.getQuota();
		}
		return sum / agents.size();
	}
	
	public void clearPlotHistory() {
		for (Plot plot : plots.getVertices()) {
			plot.resetPlotHistory();
		}
	}
	
	public List<Plot> getSortedPlots() {
		ArrayList<Plot> sorted = new ArrayList<Plot>(plots.getVertices());
		Collections.sort(sorted, Plot.PLOT_UTILITY_COMPARATOR);
		return sorted;
	}
}
